<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>16.3.1-状态机的开启与停止</title>
</head>
<body>
    <div>代码示例作为参考，此页面代码不能执行</div>
</body>
    <script>
        /**
         * 当解析器遇到开始标签时，
         *  会将该标签压入父级节点栈，同时开启新的状态机。
         * 当解析器遇到结束标签，并且父级节点栈中存在与该标签同名的开始标签节点时，
         *  会停止当前正在运行的状态机
         * 
         * ex: <div><span></div></span>
         * 
         * 1.直接比较结束标签的名称与栈顶节点的标签名称
         * - 这么做的确可行，但严格来讲是有瑕疵的
         * - 因为结束标签 </div> 缺少与之对应的开始标签，
         *    所以这时 “状态机 3” 会抛出错误：​“无效的结束标签”​。
         * 
         * 2.只要父级节点栈中存在与当前遇到的结束标签同名的节点，就停止状态机
         * - 当判断状态机是否应该停止时，我们不应该总是与栈顶的父级节点做比较，而是应该与整个父级节点栈中的所有节点做比较
         * 
         * 两者的区别体现在错误处理上
         * - 第一种解释方式，我们得到的错误信息是：​“无效的结束标签
         * - 第二种解释方式，在 “完整的内容” 部分被解析完毕后，解析器就会打印错误信息： ​“<span> 标签缺少闭合标签”​。很显然，第二种解释方式更加合理
        */


        // 定义文本模式，作为一个状态表
        const TextModes = {
            DATA: 'DATA', // 普通文本模式
            RCDATA: 'RCDATA', // 受限的 CDATA 模式
            RAWTEXT: 'RAWTEXT', // 原始文本模式
            CDATA: 'CDATA' // CDATA 模式
        }

        // 解析器函数，接收模板作为参数
        function parse(str) {
            // 定义上下文对象
            const context = {
                // source 是模板内容，用于在解析过程中进行消费
                source: str,
                // 解析器当前处于文本模式，初始模式为 DATA
                mode: TextModes.DATA
            }
            // 调用 parseChildren 函数开始进行解析，它返回解析后得到的子节点
            // parseChildren 函数接收两个参数：
            // 第一个参数是上下文对象 context
            // 第二个参数是由父代节点构成的节点栈，初始时栈为空
            const nodes = parseChildren(context, [])

            // 解析器返回 Root 根节点
            return {
                type: 'Root',
                // 使用 nodes 作为根节点的 children
                children: nodes
            }
        }

        function parseChildren(context, ancestors){
            // 定义 nodes 数组存储子节点，它将作为最终的返回值
            let nodes = []
            // 从上下文对象中取得当前状态，包括模式 mode 和模板内容 source
            const { mode, source } = context

            // 开启 while 循环，只要满足条件就会一直对字符串进行解析
            // 关于 isEnd() 后文会详细讲解
            while(!isEnd(context, ancestors)) {
                let node
                // 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
                if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
                    // 只有 DATA 模式才支持标签节点的解析
                    if (mode === TextModes.DATA && source[0] === '<') {
                        if (source[1] === '!') {
                            if (source.startsWith('<!--')) {
                                // 注释
                                node = parseComment(context)
                            } else if (source.startsWith('<![CDATA[')) {
                                // CDATA
                                node = parseCDATA(context, ancestors)
                            }
                        } else if (source[1] === '/') {
                            // 状态机遭遇了闭合标签，此时应该抛出错误，因为它缺少与之对应的开始标签
                            console.error('无效的结束标签') // 新增
                            continue // 新增
                        } else if (/[a-z]/i.test(source[1])) {
                            // 标签
                            node = parseElement(context, ancestors)
                        }
                    } else if (source.startsWith('{{')) {
                        // 解析插值
                        node = parseInterpolation(context)
                    }
                }

                // node 不存在，说明处于其他模式，即非 DATA 模式且非 RCDATA 模式
                // 这时一切内容都作为文本处理
                if (!node) {
                    // 解析文本节点
                    node = parseText(context)
                }

                // 将节点添加到 nodes 数组中
                nodes.push(node)
            }

            // 当 while 循环停止后，说明子节点解析完毕，返回子节点
            return nodes
        }

        function parseElement(context, ancestors){
            // 解析开始标签
            const element = parseTag(context)
            if (element.isSelfClosing) return element // 新增

            ancestors.push(element) // 新增
            // 这里递归地调用 parseChildren 函数进行 <div> 标签子节点的解析
            element.children = parseChildren(context, ancestors) // 新增
            ancestors.pop() // 新增

            if (context.source.startsWith(`</${element.tag}`)) { // 新增
                // 解析结束标签
                parseTag(context, 'end')
            } else {
                // 缺少闭合标签
                console.error(`${element.tag} 标签缺少闭合标签`) // 新增
            }
            
            return element
        }

        function isEnd(context, ancestors) { // 新增 isEnd 函数
            // 当模板内容解析完毕后，停止
            if (!context.source) return true

            /**
             * 直接比较结束标签的名称与栈顶节点的标签名称。
             * - “状态机 1” 遇到 <div> 开始标签，调用 parseElement 解析函数，这会开启 “状态机 2” 来完成子节点的解析
             * - “状态机 2” 遇到 <span> 开始标签，调用 parseElement 解析函数，这会开启 “状态机 3” 来完成子节点的解析
             * - “状态机 3” 遇到 </div> 结束标签。由于此时父级节点栈栈顶的节点名称是span，并不是 div，所以 “状态机 3” 不会停止运行。
             *    这时，​“状态机 3” 遭遇了不符合预期的状态，因为结束标签 </div> 缺少与之对应的开始标签，
             *    所以这时 “状态机 3” 会抛出错误：​“无效的结束标签”​。
            */
            // 获取父级标签节点
            // const parent = ancestors[ancestors.length - 1]
            // 如果遇到结束标签，并且该标签与父级标签节点同名，则停止
            // if (parent && context.source.startsWith(`</${parent.tag}`)) {
            //     return true
            // }

            // 与父级节点栈内所有节点做比较
            for (let i = ancestors.length - 1; i >= 0; --i) {
                // 只要栈中存在与当前结束标签同名的节点，就停止状态机
                if (context.source.startsWith(`</${ancestors[i].tag}`)) {
                    return true
                }
            }
        }

    </script>
    

</html>